package com.gitee.zw.rule.service;


import com.gitee.zw.rule.drools.DroolsManager;
import com.gitee.zw.rule.dao.RuleContentRepository;
import com.gitee.zw.rule.dao.RuleDomainRepository;
import com.gitee.zw.rule.model.SourceRuleDimension;
import com.gitee.zw.rule.model.SourceRuleDomain;
import com.gitee.zw.rule.utils.OperatorToExpression;
import com.gitee.zw.rule.vo.RuleContent;
import com.gitee.zw.rule.vo.RuleDomain;
import com.gitee.zw.rule.vo.RuleResult;
import com.gitee.zw.rule.vo.RuleSumResult;
import freemarker.ext.beans.BeansWrapper;
import freemarker.template.Configuration;
import freemarker.template.Template;
import freemarker.template.TemplateHashModel;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.ui.freemarker.FreeMarkerTemplateUtils;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;

import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import static java.lang.Boolean.TRUE;

@Service
@Slf4j
@Transactional(readOnly = true)
public class RuleServiceImpl implements RuleEngineService, RuleDomainService {

    final Pattern packNamePattern = Pattern.compile("^\\s*package\\s+([\\w|\\.|\\-]+)\\s*");
    final Pattern ruleNamePattern = Pattern.compile("^\\s*rule\\s+(\"(.+)\"|'(.+)')");
    @Autowired
    private DroolsManager droolsManager;
    @Autowired
    private RuleContentRepository ruleContentRepository;
    @Autowired
    private RuleDomainRepository ruleDomainRepository;


    private void replacePackage(RuleContent rule) {
        Matcher m = packNamePattern.matcher(rule.getRuleContent());
        if (m.find()) {
            log.info("replace package name from {} to {} ", m.group(1), rule.getRulePackage());
            String newCnt = m.replaceFirst("package " + rule.getRulePackage() + "\n");
            rule.setRuleContent(newCnt);
        } else {
            log.info("package name not found in rule content, add package: {}", rule.getRulePackage());
            rule.setRuleContent("package " + rule.getRulePackage() + "\n" + rule.getRuleContent());
        }
    }

    @Override
    @Transactional
    public void addOrUpdateDroolsRule(RuleContent ent) {
        replacePackage(ent);
        ruleContentRepository.save(ent);
        if (TRUE.equals(ent.getIsValid())) {
            droolsManager.addOrUpdateRule(ent);
        } else {
            removeFromEngine(ent);
        }

    }

    private void removeFromEngine(RuleContent droolsRule) {
        List<String> ruleNames = getRuleNames(droolsRule.getRuleContent());
        for (String ruleName : ruleNames) {
            droolsManager.deleteDroolsRule(droolsRule.getDomainCode(), droolsRule.getRulePackage(), ruleName);
        }
    }

    private List<String> getRuleNames(String ruleContent) {
        String[] lines = ruleContent.split("\n");
        List<String> ruleNames = new ArrayList<>();
        for (String line : lines) {
            Matcher matcher = ruleNamePattern.matcher(line);
            if (matcher.find()) {
                // group(1) 是第一个括号内的匹配结果
                String name = matcher.group(1);
                ruleNames.add(name);
            }
        }
        return ruleNames;
    }

    @Override
    @Transactional
    public void deleteDroolsRule(String code) {
        Optional<RuleContent> drOpt = ruleContentRepository.findById(code);
        if (drOpt.isPresent()) {
            RuleContent droolsRule = drOpt.get();
            ruleContentRepository.delete(droolsRule);
            removeFromEngine(droolsRule);
        }
    }

    @Override
    public List<RuleContent> getActiveRules() {
        List<RuleContent> rules = ruleContentRepository.findByIsValid(true);
        return rules;
    }

    @Override
    public RuleSumResult fireRule(@PathVariable String domain, @RequestBody Map<String, Object> params) {
        if (!droolsManager.existsKieBase(domain)) {
            throw new IllegalArgumentException("Domain " + domain + " not found in session");
        }
        Optional<RuleDomain> ruleDomOpt = ruleDomainRepository.findById(domain);
        if (!ruleDomOpt.isPresent()) {
            throw new IllegalArgumentException("Domain " + domain + " not found in db");
        }
        RuleDomain ruleDomain = ruleDomOpt.get();
        return droolsManager.fireRule(ruleDomain, params);
    }

    @Override
    public RuleResult<?> executeRules(@PathVariable String domain, List<Map<String, Object>> dataList) throws Exception {
        if (!droolsManager.existsKieBase(domain)) {
            throw new IllegalArgumentException("Domain " + domain + " not found in session");
        }
        Optional<RuleDomain> ruleDomain = ruleDomainRepository.findById(domain);
        if (!ruleDomain.isPresent()) {
            throw new IllegalArgumentException("Domain " + domain + " not found in db");
        }

        List dataList1 = dataList;
        return droolsManager.execRule(ruleDomain.get(), dataList1, Map.of());
    }

    @Override
    public List<RuleContent> getAllRules() {
        return ruleContentRepository.findAll();
    }

    @Override
    public List<RuleDomain> getAllDomain() {
        return ruleDomainRepository.findAll();
    }

    @Override
    public RuleContent getRuleContent(String code) {
        RuleContent res = ruleContentRepository.findById(code).orElse(null);
        if (res == null) {
            throw new RuntimeException("code not found: " + code);
        }
        return ruleContentRepository.findById(code).orElse(null);
    }

    public void startUpEngine() {
        ruleContentRepository.findByIsValid(true).forEach(droolsRule -> {
            log.info("Adding rule {}", droolsRule.getCode());
            droolsManager.addOrUpdateRule(droolsRule);
        });
    }

    private static void addBeanPrefix(SourceRuleDimension dim, Map<String, String> map) {
        if (dim.getValue() != null) {
            String[] vals = dim.getValue().splitWithDelimiters(" |\\+|-|\\*|/|,|\\[|\\]", 0);
            for (int i = 0; i < vals.length; i++) {
                String val = vals[i].trim();
                if (!val.startsWith("\"") && !val.startsWith("'") && map.containsKey(val)) {
                    vals[i] = "$c." + vals[i].trim();
                }
            }
            dim.setValue(String.join("", vals));
        }
    }

    /**
     * 去掉冗余的逻辑，格式优化
     * 1、if (ture && a == b ... => if (a == b ...)
     * 2、a == true/TRUE/false/FALSE >> a == Boolean.TRUE/Boolean.FALSE
     * 3、else 语句前面多余的换行去除
     *
     * @param script
     * @return
     */
    private static String optimizeScript(String script) {
        return script.replaceAll("if \\(true && ", "if (")
                .replaceAll(" == true", " == Boolean.TRUE")
                .replaceAll(" == false", " == Boolean.FALSE")
                .replaceAll(" == TRUE", " == Boolean.TRUE")
                .replaceAll(" == FALSE", " == Boolean.FALSE")
                .replaceAll("\\}\\s*\\n\\s*else", "} else");
    }

    public String genRuleScript(SourceRuleDomain sourceRuleDomain) throws Exception {
        if (sourceRuleDomain.getFactClass() == null) {
            Optional<RuleDomain> rd = ruleDomainRepository.findById(sourceRuleDomain.getCode());
            if (!rd.isPresent()) {
                throw new IllegalArgumentException("Domain " + sourceRuleDomain.getCode() + " not found in db");
            }
            sourceRuleDomain.setFactClass(rd.get().getFactClass());
        }
        Configuration configuration = new Configuration(Configuration.VERSION_2_3_33);
        configuration.setDefaultEncoding("utf-8");
        configuration.setClassForTemplateLoading(this.getClass(), "/ftl");
        Template template = configuration.getTemplate("drools.ftl");
        //生成字段名称、类型列表
        Map<String, String> map = new HashMap<>();
        sourceRuleDomain.getDimensionTypes().forEach(dimType -> {
            map.put(dimType.getCode(), dimType.getDataType());
        });
        //字段名加前缀
        sourceRuleDomain.getRules().forEach(rule -> {
            if (rule.getBranches() != null) {
                rule.getBranches().forEach(branch -> {
                    branch.getDimensions().forEach(dim -> {
                        addBeanPrefix(dim, map);
                    });
                    branch.getTargets().forEach(target -> {
                        addBeanPrefix(target, map);
                    });
                });
            }
        });
        BeansWrapper wrapper = BeansWrapper.getDefaultInstance();
        TemplateHashModel staticModels = wrapper.getStaticModels();
        TemplateHashModel expressionUtils = (TemplateHashModel) staticModels.get(OperatorToExpression.class.getName());
        Map params = Map.of(
                "ruleDomain", sourceRuleDomain,
                "dimensionTypes", map,
                "ExpressionUtils", expressionUtils);
        String script = FreeMarkerTemplateUtils.processTemplateIntoString(template, params);
        script = optimizeScript(script);
        return script;
    }

    @Override
    public void deployRuleDomain(SourceRuleDomain sourceRuleDomain) throws Exception {
        Optional<RuleDomain> rd = ruleDomainRepository.findById(sourceRuleDomain.getCode());
        if (!rd.isPresent()) {
            throw new IllegalArgumentException("Domain " + sourceRuleDomain.getCode() + " not found in db");
        }
        RuleDomain ruleDomain = rd.get();
        String script = genRuleScript(sourceRuleDomain);
        List<RuleContent> ruleContentList = ruleContentRepository.findByDomainCode(ruleDomain.getCode());
        for (RuleContent ruleContent : ruleContentList) {
            removeFromEngine(ruleContent);
        }
        ruleContentRepository.deleteAll(ruleContentList);
        RuleContent ruleContent = new RuleContent();
        ruleContent.setCode(sourceRuleDomain.getCode())
                //.setDescription(sourceRuleDomain.get())
                .setDomainCode(sourceRuleDomain.getCode())
                .setIsValid(true)
                .setRuleContent(script);
        ruleContentRepository.save(ruleContent);
        droolsManager.addOrUpdateRule(ruleContent);
    }
}
